use std::sync::Arc;

use futures::TryFutureExt;
use tracing::*;

use aspect_db_common::device::{DeviceDatastore, DirEntry};
use aspect_ident::{DeviceIdent, DirPackage};

use crate::errors::Error;
use crate::peer_manager;

pub struct DeviceResolver {
    peer_man: Arc<peer_manager::PeerManager>,
    ident_ds: Arc<DeviceDatastore>,
}

impl DeviceResolver {
    pub fn new(peer_man: Arc<peer_manager::PeerManager>, ident_ds: Arc<DeviceDatastore>) -> Self {
        Self { peer_man, ident_ds }
    }

    /// Used to store a device package when we have one from a remote.
    async fn store_device_from_remote(&self, dirpkg: &DirPackage) -> Result<(), Error> {
        let ident = *dirpkg.last_update().ident();
        info!(%ident, "storing new device entry");
        let dent = DirEntry::Pkg(dirpkg.clone());
        let res = self.ident_ds.try_insert_dir_entry(&dent).await?;
        if res.is_some() {
            warn!(%ident, "had apparently better device entry when storing what we thought was a better one");
        }
        Ok(())
    }

    /// Looks up a device, first locally then from the connected homeserver.
    pub async fn lookup_device(&self, devident: &DeviceIdent) -> Result<DirPackage, Error> {
        match self.ident_ds.query_dir_entry(devident).await? {
            Some(de) => match de {
                DirEntry::Pkg(pkg) => Ok(pkg),
                DirEntry::Equiv(_) => Err(Error::DeviceEquivocated(*devident)),
            },
            None => {
                let client = self
                    .peer_man
                    .get_homeserver_client_ok()
                    .map_err(|_| Error::NoHomeserver)
                    .await?;
                match client.get_device_id(*devident).await? {
                    Some(dirpkg) => {
                        self.store_device_from_remote(&dirpkg).await?;
                        Ok(dirpkg)
                    }
                    None => Err(Error::UnknownDeviceIdent(*devident)),
                }
            }
        }
    }

    /// Looks up devices for a user through our homeserver.
    pub async fn lookup_user_devices(&self, name: &str) -> Result<Vec<DeviceIdent>, Error> {
        let client = self
            .peer_man
            .get_homeserver_client_ok()
            .map_err(|_| Error::NoHomeserver)
            .await?;

        let dev_list = client.get_user_devices(name.to_owned()).await?;
        let mut idents = Vec::new();

        for dirpkg in &dev_list {
            let ident = *dirpkg.last_update().ident();
            idents.push(ident);
            if let Err(e) = self.store_device_from_remote(&dirpkg).await {
                // TODO make this a hard error?
                warn!(err = %e, %ident, %name, "error storing device for user that was returned in search query");
            }
        }

        Ok(idents)
    }
}
